using System.Collections.Generic;

using UnityEngine;

#if UNITY_2017_3_OR_NEWER
using UnityEngine.Profiling;
#endif

namespace Voxels {
	public sealed partial class Chunk {
		public const int CHUNK_SIZE_X                         = 16;
		public const int CHUNK_SIZE_Y                         = 16;
		public const int CHUNK_SIZE_Z                         = 16;
		public const int CHUNK_MESH_CAPACITY                  = 8192;
		public const int MAX_DIRTY_BLOCKS_BEFORE_FULL_REBUILD = 128;
		public const int MESHER_CAPACITY                      = 2048;

#if UNITY_2017_3_OR_NEWER
		public ChunkRenderer Renderer     { get; set; }
#endif
		public Vector3       OriginPos    { get; }
		public Vector3Int    OriginPosInt {	get; }

		public int Index { get; private set; }

		public int BlockCount           { get; private set; }
		public bool Dirty               { get; private set; } = false;
		public bool NeedRebuildGeometry { get; set; }         = false;


		IChunkMesher        _mesher  = null;
		BlockInfoProvider   _library = null;

		bool    _needUpdateVisibilityAll     = true;

		VisibilityFlagsHolder _visibiltiy   = null;
		BlockDataHolder       _blocks       = null;
		List<Vector3Int>      _dirtyBlocks  = new List<Vector3Int>(MAX_DIRTY_BLOCKS_BEFORE_FULL_REBUILD);

		public void FinishInitClientChunk() {
			_library = StaticResources.BlocksInfo;
			_mesher = new ChunkMesher(_library, CHUNK_MESH_CAPACITY, MESHER_CAPACITY, OriginPos);
			SetDirtyAll();
		}

		public Chunk(ChunkData data) {
			var b = data.Blocks;
			Index = data.Index;
			_visibiltiy   = new VisibilityFlagsHolder(CHUNK_SIZE_X, CHUNK_SIZE_Y, CHUNK_SIZE_Z);
			_blocks       = new BlockDataHolder(b.SizeX, b.SizeY, b.SizeZ, b.Data);
			OriginPos     = data.Origin;
			OriginPosInt = new Vector3Int(Mathf.FloorToInt(data.Origin.x), Mathf.FloorToInt(data.Origin.y), Mathf.FloorToInt(data.Origin.z));
			_library      = StaticResources.BlocksInfo;

			_mesher = new ChunkMesher(_library, CHUNK_MESH_CAPACITY, MESHER_CAPACITY, OriginPos);
			Dirty = true;
			_needUpdateVisibilityAll= true;
		}

		public Chunk(Vector3 originPos, int index) {
			Index = index;
			_visibiltiy   = new VisibilityFlagsHolder(CHUNK_SIZE_X, CHUNK_SIZE_Y, CHUNK_SIZE_Z);
			_blocks       = new BlockDataHolder      (CHUNK_SIZE_X, CHUNK_SIZE_Y, CHUNK_SIZE_Z);
			OriginPos     = originPos;
			OriginPosInt = new Vector3Int(Mathf.FloorToInt(originPos.x), Mathf.FloorToInt(originPos.y), Mathf.FloorToInt(originPos.z));
			_library      = StaticResources.BlocksInfo;

			_mesher = new ChunkMesher(_library, CHUNK_MESH_CAPACITY, MESHER_CAPACITY, originPos);
			Dirty = true;
			_needUpdateVisibilityAll = true;
		}

		public ChunkData GetData() {
			return new ChunkData() { Blocks = _blocks, Origin = OriginPos };
		}

		public void SetAllBlocks(BlockData[] blocks) {
			_blocks.Data = blocks;
		}

		public GeneratableMesh OpaqueCollidedMesh {
			get {
				return _mesher.OpaqueCollidedMesh;
			}
		}

		public override string ToString() {
			return string.Format("{0}", Index);
		}

		public float GetDistance(Vector3 pos) {
			pos.y = 0;
			return Vector3.Distance(pos, OriginPos);
		}

		public void UnloadChunk() {
#if UNITY_2017_3_OR_NEWER
			Renderer = null;
#endif
			_mesher.DeInit();
		}

		public void UpdateGeometry() {
			if ( _mesher.Busy ) {
				return;
			}
			_mesher.PrepareMesher();
			_mesher.InitData(this);
			_mesher.StartAsyncMeshing();
			NeedRebuildGeometry = false;
		}

		public void PrepareAsyncMeshing() {
			var blockList = _mesher.Blocks;
			if ( blockList == null ) {
				NeedRebuildGeometry = false;
				return;
			}

			var maxVal = CHUNK_SIZE_X * CHUNK_SIZE_Y * CHUNK_SIZE_Z;
			var divY = CHUNK_SIZE_X * CHUNK_SIZE_Z;
			var divz = CHUNK_SIZE_X;
			for ( int i = 0; i < maxVal; i++ ) {
				var block = _blocks[i];
				if ( _visibiltiy[i] == VisibilityFlags.None || block.IsEmpty()  ) {
					continue;
				}
				var y = i / divY;
				var t = i % divY;
				var z = t / divz;
				var x = t % divz;
				blockList.Add(new MesherBlockInput() {
					Block = block,
					Position = new Byte3((byte)x, (byte)y, (byte)z),
					Visibility = _visibiltiy[i]
				});
			}
		}

		public void UpdateVisibilityAll(int from, int to) {
#if UNITY_2017_3_OR_NEWER
			//Profiler.BeginSample(string.Format("Visibility Full Update {0}", ToString()));
#endif
			//var watch = System.Diagnostics.Stopwatch.StartNew();
			_dirtyBlocks.Clear();
			_needUpdateVisibilityAll = false;
			NeedRebuildGeometry = true;

			var fromVal = (from) * CHUNK_SIZE_X * CHUNK_SIZE_Z;
			var maxVal = to * CHUNK_SIZE_X * CHUNK_SIZE_Z;
			var divY = CHUNK_SIZE_X * CHUNK_SIZE_Z;
			var divz = CHUNK_SIZE_X;
			var arr = _blocks.Data;

			var blockCount = 0;

			for ( int i = fromVal; i < maxVal; i++ ) {
				if ( arr[i].IsEmpty() ) {
					_visibiltiy[i] = VisibilityFlags.None;
					continue;
				}
				blockCount++;
				var y = i / divY;
				var t = i % divY;
				var z = t / divz;
				var x = t % divz;
				_visibiltiy[i] = CheckVisibilityOpaque(x, y, z);
			}
			BlockCount = blockCount;
			//watch.Stop();
#if UNITY_2017_3_OR_NEWER
			//Profiler.EndSample();
#endif
		}

		public void UpdateChunk() {
			UpdateVisibilityForDirtyBlocks();
		}

		public void ForceUpdateChunk() {
			_needUpdateVisibilityAll = true;
			UpdateChunk();
		}

		//TODO: убрать синхронный апдейт видимости
		public void UpdateVisibilityForDirtyBlocks() {
			if ( _needUpdateVisibilityAll ) {
				UpdateVisibilityAll(0, CHUNK_SIZE_Y);
			}
			foreach ( var block in _dirtyBlocks ) {
				UpdateVisibilityForNeighbors(block.x, block.y, block.z);
			}
			_dirtyBlocks.Clear();
			Dirty = false;
			NeedRebuildGeometry = true;
		}

		void UpdateVisibilityAtBlock(int x, int y, int z) {
			var block = _blocks[x, y, z];
			if ( block.IsEmpty() ) {
				_visibiltiy[x, y, z] = VisibilityFlags.None;
				return;
			}
			_visibiltiy[x, y, z] = CheckVisibilityOpaque(x, y, z);
		}

		void UpdateVisibilityForNeighbors(int x, int y, int z) {
			UpdateVisibilityAtBlock(x, y, z); // self
			//Up
			if ( y < CHUNK_SIZE_Y - 1 ) {
				UpdateVisibilityAtBlock(x, y + 1, z);
			}			
			//Down
			if ( y > 0 ) {
				UpdateVisibilityAtBlock(x, y - 1, z);
			}
			//Right
			if ( x < CHUNK_SIZE_X - 1 ) {
				UpdateVisibilityAtBlock(x + 1, y, z);
			}
			//Left
			if ( x > 0 ) {
				UpdateVisibilityAtBlock(x - 1, y, z);
			}
			//Forward
			if ( z < CHUNK_SIZE_Z - 1 ) {
				UpdateVisibilityAtBlock(x, y, z + 1);
			}
			//Backwards
			if ( z > 0 ) {
				UpdateVisibilityAtBlock(x, y, z - 1);
			}
		}

		public bool HasBlockAt(int x, int y, int z) {
			return !_blocks[x, y, z].IsEmpty();
		}

		public bool HasFullOpaqueBlockAt(int x, int y, int z) {
			var type = _blocks[x, y, z].Type;
			return !GetBlockTranslucent(type) && GetBlockFull(type);
		}

		public bool HasTranslucentOrNonFullBlockAt(int x, int y, int z) {
			return _library.IsLightPassBlock(_blocks[x, y, z].Type);
		}

		public bool HasFullBlockAt(int x, int y, int z) {
			if ( HasBlockAt(x, y, z) ) {
				return GetBlockFull(_blocks[x, y, z].Type);
			}
			return false;
		}

		public void PutBlock(Byte3 pos, BlockData block) {
			if ( pos.X >= CHUNK_SIZE_X || pos.X >= CHUNK_SIZE_Y || pos.X >= CHUNK_SIZE_Z ) {
				return;
			}
			PutBlock(pos.X, pos.Y, pos.Z, block);
		}

		public void PutBlock(int x, int y, int z, BlockData block) {
			var oldBlock = _blocks[x, y, z];
			_blocks[x, y, z] = block;
			AddDirtyBlock(x, y, z);
		}

		public Vector3Int GetBlockPosFromWorldPos(Vector3 worldPos) {
			var localPos = worldPos - OriginPos;
			var local = new Vector3Int(Mathf.FloorToInt(localPos.x), Mathf.FloorToInt(localPos.y), Mathf.FloorToInt(localPos.z));
			return local;
		}

		public BlockData GetBlock(int x, int y, int z) {
			if ( x<0 || y <0 || z < 0 ) {
				return BlockData.Empty;
			}
			return _blocks[x, y, z];
		}

		public void RemoveBlock(int x, int y, int z) {
			if ( _blocks[x, y, z].IsEmpty() ) { return; }
			_blocks[x, y, z] = BlockData.Empty;
			BlockCount--;
			AddDirtyBlock(x, y, z);
		}

		public Vector3Int LocalToGlobalCoordinates(int x, int y, int z) {
			return OriginPosInt + new Vector3Int(x, y, z);
		}

		void AddDirtyBlock(int x, int y, int z) {
			Dirty = true;
			if ( _needUpdateVisibilityAll || _dirtyBlocks.Count >= MAX_DIRTY_BLOCKS_BEFORE_FULL_REBUILD ) {
				_needUpdateVisibilityAll = true;
				return;
			}
			_dirtyBlocks.Add(new Vector3Int(x, y, z));
		}

		public void SetDirtyAll() {
			Dirty = true;
			_needUpdateVisibilityAll = true;
		}

		BlockDescription GetBlockDescription(BlockType type) {
			return _library.GetBlockDescription(type);
		}

		bool GetBlockTranslucent(BlockType type) {
			return _library.IsTranslucentBlock(type);
		}

		bool GetBlockFull(BlockType type) {
			return _library.IsFullBlock(type);
		}

		VisibilityFlags CheckVisibilityOpaque(int x, int y, int z) {
			var flag = VisibilityFlags.None;
			//Up
			if ( y < CHUNK_SIZE_Y - 1 ) {
				if ( _library.IsLightPassBlock(_blocks[x, y + 1, z].Type) ) {
					VisibilityFlagsHelper.Set(ref flag, VisibilityFlags.Up);
				}
			} else {
				VisibilityFlagsHelper.Set(ref flag, VisibilityFlags.Up);
			}
			//Down
			if ( y > 0 ) {
				if ( _library.IsLightPassBlock(_blocks[x, y - 1, z].Type) ) {
					VisibilityFlagsHelper.Set(ref flag, VisibilityFlags.Down);
				}
			} else {
				VisibilityFlagsHelper.Set(ref flag, VisibilityFlags.Down);
			}
			//Right
			if ( x < CHUNK_SIZE_X - 1 ) {
				if ( _library.IsLightPassBlock(_blocks[x + 1, y, z].Type) ) {
					VisibilityFlagsHelper.Set(ref flag, VisibilityFlags.Right);
				}
			}	else {
				VisibilityFlagsHelper.Set(ref flag, VisibilityFlags.Right);
			}
			//Left
			if ( x > 0 ) {
				if ( _library.IsLightPassBlock(_blocks[x - 1, y, z].Type) ) {
					VisibilityFlagsHelper.Set(ref flag, VisibilityFlags.Left);
				}
			} else {
				VisibilityFlagsHelper.Set(ref flag, VisibilityFlags.Left);
			}
			//Forward
			if ( z < CHUNK_SIZE_Z - 1 ) {
				if ( _library.IsLightPassBlock(_blocks[x, y, z + 1].Type) ) {
					VisibilityFlagsHelper.Set(ref flag, VisibilityFlags.Forward);
				}
			} else {
				VisibilityFlagsHelper.Set(ref flag, VisibilityFlags.Forward);
			}
			//Backwards
			if ( z > 0 ) {
				if ( _library.IsLightPassBlock(_blocks[x, y, z - 1].Type) ) {
					VisibilityFlagsHelper.Set(ref flag, VisibilityFlags.Backward);
				}
			} else {
				VisibilityFlagsHelper.Set(ref flag, VisibilityFlags.Backward);
			}
			return flag;
		}
	}
}
